Blocks 中的状态 (State in Blocks)
在使用 gr.Blocks()
构建 Gradio 应用程序时,您可能需要在用户之间共享某些值(例如,网页访问计数器),或者为单个用户在不同交互之间保持某些值(例如,聊天历史记录)。这被称为状态管理,在 Gradio 应用程序中有三种管理状态的方式:
- 全局状态(Global State):在 Gradio 应用程序运行期间,在所有用户之间保持并共享值。
- 会话状态(Session State):在单个用户使用 Gradio 应用程序的单次会话期间保持值。如果用户刷新页面,会话状态将被重置。
- 浏览器状态(Browser State):将值保存在浏览器的本地存储中,即使页面刷新或关闭后,数据仍然可以保持。
全局状态 (Global State)
Gradio 应用中的全局状态非常简单:在函数外部创建的任何变量都会在所有用户之间全局共享。
这使得管理全局状态变得非常简单,且无需外部服务。例如,在以下应用程序中,visitor_count
变量在所有用户之间共享:
import gradio as gr
# 在所有用户之间共享
visitor_count = 0
def increment_counter():
global visitor_count
visitor_count += 1
return visitor_count
with gr.Blocks() as demo:
number = gr.Textbox(label="访问者总数", value="计数中...")
demo.load(increment_counter, inputs=None, outputs=number)
demo.launch()
这意味着,如果您不希望在用户之间共享某个值,则应该在函数内部声明它。但是,如果您需要在函数调用之间共享值(例如聊天历史记录),那么应该使用以下状态管理方法之一。
会话状态 (Session State)
Gradio 支持会话状态,数据可以在页面会话中的多次提交之间保持。需要再次强调,会话数据不会在应用程序的不同用户之间共享,如果用户刷新页面重新加载 Gradio 应用程序,数据也不会保持。要在会话状态中存储数据,您需要做三件事:
- 创建一个
gr.State()
对象。如果这个状态对象有默认值,请将其传递给构造函数。注意,gr.State
对象必须是可深度复制的,否则您需要使用不同的方法(如下所述)。 - 在事件监听器中,根据需要将
State
对象作为输入和输出。 - 在事件监听器函数中,添加变量到输入参数和返回值中。
让我们看一个简单的例子。下面是一个简单的购物车应用,您可以向购物车中添加商品,并查看购物车的大小:
import gradio as gr
with gr.Blocks() as demo:
cart = gr.State([]) # 初始化为空列表
items_to_add = gr.CheckboxGroup(["谷物", "牛奶", "橙汁", "水"])
def add_items(new_items, previous_cart):
cart = previous_cart + new_items
return cart
gr.Button("添加商品").click(add_items, [items_to_add, cart], cart)
cart_size = gr.Number(label="购物车大小")
cart.change(lambda cart: len(cart), cart, cart_size)
demo.launch()
请注意我们如何使用状态:
- 我们在
gr.State()
对象中存储购物车商品,初始化为空列表。 - 向购物车添加商品时,事件监听器同时使用购物车作为输入和输出 - 它返回更新后的包含所有商品的购物车。
- 我们可以为购物车添加
.change
监听器,同样将状态变量作为输入。
您可以将 gr.State
视为一个不可见的 Gradio 组件,可以存储任何类型的值。在这里,cart
在前端不可见,但用于计算。
对于状态变量,当任何事件监听器更改状态变量的值时,.change
监听器将被触发。如果状态变量包含序列(如 list
、set
或 dict
),当其中的任何元素发生变化时,都会触发更改。如果它包含对象或基本类型,当值的哈希发生变化时,会触发更改。因此,如果您定义了一个自定义类并创建了该类实例的 gr.State
变量,请确保该类包含合理的 __hash__
实现。
当用户刷新页面时,会话状态变量的值会被清除。在用户关闭标签页后,该值会在应用后端存储 60 分钟(这可以通过 gr.Blocks
的 delete_cache
参数配置)。
处理不可深度复制的对象
如前所述,存储在 gr.State
中的值必须是可深度复制的。如果您使用的是不能深度复制的复杂对象,可以采用不同的方法,手动读取用户的 session_hash
并为每个用户存储一个全局 dictionary
,其中包含您对象的实例。以下是操作方法:
import gradio as gr
class NonDeepCopyable:
def __init__(self):
from threading import Lock
self.counter = 0
self.lock = Lock() # Lock 对象不能被深度复制
def increment(self):
with self.lock:
self.counter += 1
return self.counter
# 存储用户特定实例的全局字典
instances = {}
def initialize_instance(request: gr.Request):
instances[request.session_hash] = NonDeepCopyable()
return "会话已初始化!"
def cleanup_instance(request: gr.Request):
if request.session_hash in instances:
del instances[request.session_hash]
def increment_counter(request: gr.Request):
if request.session_hash in instances:
instance = instances[request.session_hash]
return instance.increment()
return "错误:会话未初始化"
with gr.Blocks() as demo:
output = gr.Textbox(label="状态")
counter = gr.Number(label="计数器值")
increment_btn = gr.Button("增加计数器")
increment_btn.click(increment_counter, inputs=None, outputs=counter)
# 页面加载时初始化实例
demo.load(initialize_instance, inputs=None, outputs=output)
# 页面关闭/刷新时清理实例
demo.close(cleanup_instance)
demo.launch()
浏览器状态 (Browser State)
Gradio 还支持浏览器状态,即使在页面刷新或关闭后,数据也会保存在浏览器的 localStorage 中。这对于存储用户偏好、设置、API 密钥或其他应该在会话之间保持的数据非常有用。要使用浏览器状态:
- 创建一个
gr.BrowserState
对象。您可以选择提供初始默认值和标识浏览器 localStorage 中数据的键。 - 在事件监听器中,将其作为常规
gr.State
组件用作输入和输出。
以下是一个在会话之间保存用户名和密码的简单示例:
import random
import string
import gradio as gr
import time
with gr.Blocks() as demo:
gr.Markdown("您的用户名和密码将保存在浏览器的本地存储中。"
"如果您刷新页面,这些值将被保留。")
username = gr.Textbox(label="用户名")
password = gr.Textbox(label="密码", type="password")
btn = gr.Button("随机生成")
local_storage = gr.BrowserState(["", ""])
saved_message = gr.Markdown("✅ 已保存到本地存储", visible=False)
@btn.click(outputs=[username, password])
def generate_randomly():
u = "".join(random.choices(string.ascii_letters + string.digits, k=10))
p = "".join(random.choices(string.ascii_letters + string.digits, k=10))
return u, p
@demo.load(inputs=[local_storage], outputs=[username, password])
def load_from_local_storage(saved_values):
print("从本地存储加载", saved_values)
return saved_values[0], saved_values[1]
@gr.on([username.change, password.change], inputs=[username, password], outputs=[local_storage])
def save_to_local_storage(username, password):
return [username, password]
@gr.on(local_storage.change, outputs=[saved_message])
def show_saved_message():
timestamp = time.strftime("%I:%M:%S %p")
return gr.Markdown(
f"✅ 已在 {timestamp} 保存到本地存储",
visible=True
)
demo.launch()
注意:存储在 gr.BrowserState
中的值在 Gradio 应用重启时不会保持。要保持它,可以在 gr.BrowserState
组件中硬编码特定的 storage_key
和 secret
值,并在相同的服务器名称和服务器端口上重新启动 Gradio 应用。但是,只有在运行受信任的 Gradio 应用时才应该这样做,因为原则上,这可以允许一个 Gradio 应用访问由另一个 Gradio 应用创建的 localStorage 数据。
状态管理的最佳实践
在选择状态管理方法时,请考虑以下最佳实践:
- 全局状态:适用于需要在所有用户之间共享的数据,如访问计数器或全局设置。
- 会话状态:对于特定于用户的数据,如当前会话的交互历史,但不需要长期保持。
- 浏览器状态:对于应该在浏览器会话之间保持的用户特定数据,如个人偏好或认证令牌。
根据您的应用程序需求选择合适的状态管理方法,可以提高用户体验并简化代码。
资源清理
长时间运行的应用程序可能会积累大量状态数据。Gradio 提供了一些工具来帮助清理资源:
- 自动删除
gr.State
:当用户关闭浏览器标签页时,Gradio 会在 60 分钟后自动删除与该用户会话关联的任何gr.State
变量。 - 自动缓存清理:使用
delete_cache
参数可以定期清理临时文件。 Blocks.unload
事件:可以在用户断开连接时运行任意清理函数。
import gradio as gr
with gr.Blocks(delete_cache=(86400, 86400)) as demo: # 每天删除超过一天的临时文件
# 应用程序代码...
# 用户关闭标签页时清理资源
demo.unload(lambda: print("用户已离开,清理资源"))
demo.launch()
结论
状态管理是构建复杂 Gradio 应用程序的关键方面。通过了解全局状态、会话状态和浏览器状态之间的差异,您可以为您的应用程序选择最合适的方法,从而创建更加动态和响应式的用户体验。
在下一章中,我们将探讨如何像函数一样使用 Blocks,这使您可以轻松组合和重用功能组件。